<?php

namespace EchoPHP\Foundation;

use ArrayAccess;
use ArrayIterator;
use Closure;
use Countable;
use Exception;
use InvalidArgumentException;
use IteratorAggregate;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use Reflector;

class Container implements ArrayAccess, Countable, IteratorAggregate {

    /**
     * 容器对象实例
     * @var
     */
    protected static $instance;

    /**
     * 容器对象中的对象实例
     * @var array
     */
    protected $instances = [];

    /**
     * 绑定标识
     * @var array
     */
    protected $bindings = [];

    /**
     * 绑定标识别名
     * @var array
     */
    protected $aliases = [];

    /**
     * 设置容器实例 一般会实例化Container的子类Application作为容器，此时可以通过setInstance 方式避免重复初始化
     * @param \spy\Container $instance
     * @return void
     */
    public static function setInstance($instance) {
        static::$instance = $instance;
    }

    /**
     * 获取容器实例
     * self:: 和 static:: 的區別
     * self:: 获取当前类
     * static:: 获取父类
     * @return static
     */
    public static function getInstance() {
        if (is_null(static::$instance)) {
            static::$instance = new static;
        }
        return static::$instance;
    }

    /**
     * 判断是否已经绑定容器标识
     * @param $abstract
     * @return bool
     */
    public function bound($abstract) {
        $abstract = $this->getAlias($abstract);
        return isset($this->bindings[$abstract]) ||
            isset($this->instances[$abstract]);
    }

    /**
     * 判断容器中是否存在对象实例
     * @access public
     * @param string $abstract 类名或者标识
     * @return bool
     */
    public function exists($abstract) {
        $abstract = $this->getAlias($abstract);
        return isset($this->instances[$abstract]);
    }

    /**
     * 判断绑定是否单例
     * @param string $abstract
     * @return bool
     */
    public function isShared($abstract) {
        $abstract = $this->getAlias($abstract);
        return isset($this->instances[$abstract]) ||
            (isset($this->bindings[$abstract]['shared']) &&
                $this->bindings[$abstract]['shared'] === true);
    }

    /**
     * 设置绑定标识别名
     * @param $abstract
     * @param $alias
     * @throws Exception
     */
    public function setAlias($abstract, $alias) {
        $this->aliases[$alias] = $abstract;
    }

    /**
     * 根据别名获取真实的绑定标识名
     * @param $abstract
     * @return mixed
     */
    public function getAlias($abstract) {
        return isset($this->aliases[$abstract]) ? $this->aliases[$abstract] : $abstract;
    }

    /**
     * 绑定容器标识
     * @param string $abstract 容器绑定标识
     * @param null|string|Closure $concrete 绑定实体 即 要绑定的类（接口实现也是类）、闭包或者实例
     * @param bool $shared 是否共享即是否注册为单例模式
     * @return void
     */
    public function bind($abstract, $concrete = null, $shared = false) {
        $this->dropStaleInstances($abstract);
        if (is_null($concrete)) {
            $concrete = $abstract;
        }
        if (!$concrete instanceof Closure) {
            if (is_object($concrete)) {
                //如果绑定的是类实例化对象的话，直接将其配置到实例化集合中，后续当成单例模式使用
                $this->instances[$abstract] = $concrete;
                return;
            }
        }
        $this->bindings[$abstract] = compact('concrete', 'shared');
    }

    /**
     * 绑定容器标识，如果已经绑定则不重新绑定
     * @param string $abstract
     * @param null $concrete
     * @param bool $shared
     * @return void
     */
    public function bindIf($abstract, $concrete = null, $shared = false) {
        if (!$this->bound($abstract)) {
            $this->bind($abstract, $concrete, $shared);
        }
    }

    /**
     * 绑定单例模式
     * @param string $abstract
     * @param null $concrete
     * @return void
     */
    public function singleton($abstract, $concrete = null) {
        $this->bind($abstract, $concrete, true);
    }

    /**
     * 绑定类对象到容器，如果绑定标识已经定义，那么此绑定标识会被覆盖为单例模式,使用instance不会覆盖已经绑定的绑定标识
     * 也可以用户字符串绑定，如绑定全局文件路径
     * 如  bindings 'config' ==> Config::class，非单例
     *     alias:  'myconfig' => 'config'
     *      此时使用instance方法来绑定  'config' => (Object)(Config::class)
     *      如果使用 myconfig 作为对象的绑定标识，实际存储的也会是 config 这个真实的绑定标识
     * @param string $abstract 绑定标识
     * @param object $concrete 只能绑定类实例化对象
     */
    public function instance($abstract, $concrete) {
        $abstract = $this->getAlias($abstract);
        $this->instances[$abstract] = $concrete;
    }

    /**
     * 构造绑定标识的类的实例化对象
     * @param $abstract
     * @param array $parameters
     * @return mixed|object
     * @throws Exception
     */
    public function make($abstract, array $parameters = []) {
        $abstract = $this->getAlias($abstract);
        if (isset($this->instances[$abstract])) {
            //如果类的实例化对象已经存在，说明这是一个单例模式，无需重新构建类实例
            return $this->instances[$abstract];
        }
        if (isset($this->bindings[$abstract])) {
            $concrete = $this->bindings[$abstract]['concrete'];
            $object = $this->build($concrete, $parameters);
        } else {
            $object = $this->invokeClass($abstract, $parameters);
        }

        if ($this->isShared($abstract)) {
            //如果是共享的单例模式，那么直接将构建出来的类实例放入 $instances中去，下次就无需再次构建直接返回类对象实例
            $this->instances[$abstract] = $object;
        }
        return $object;
    }

    /**
     * 构建绑定对象的实例
     * @param $concrete 被绑定的实例， 类或者闭包
     * @param array $parameters
     * @return mixed|object
     * @throws Exception
     */
    public function build($concrete, array $parameters = []) {
        if ($concrete instanceof Closure) {
            return $this->invokeFunction($concrete, $parameters);
        } else {
            $object = $this->invokeClass($concrete, $parameters);
        }

        return $object;
    }

    /**
     * 删除容器中得实例
     * @param $abstract
     */
    public function delete($abstract) {
        if (isset($this->instances[$abstract])) {
            unset($this->instances[$abstract]);
        }
    }

    /**
     * 清空容器
     */
    public function flush() {
        $this->instances = [];
        $this->bindings = [];
        $this->aliases = [];
    }

    /**
     * 通过反射执行回调函数
     * @param $callable
     * @param array $params
     * @return mixed
     * @throws Exception
     */
    public function invoke($callable, $params = []) {
        if ($callable instanceof Closure) {
            return $this->invokeFunction($callable, $params);
        }

        return $this->invokeMethod($callable, $params);
    }

    /**
     * 执行函数或者闭包方法 支持参数调用
     * @param $function
     * @param array $params
     * @return mixed
     * @throws Exception
     */
    public function invokeFunction($function, $params = []) {
        try {
            $reflect = new ReflectionFunction($function);
            $args = $this->bindParams($reflect, $params);

            return call_user_func_array($function, $args);
        } catch (ReflectionException $e) {
            throw new Exception('function not exists: ' . $function . '()');
        }
    }

    /**
     * 调用反射执行类的方法 支持参数绑定
     * @param $method       方法
     * @param array $params 参数
     * @return mixed
     * @throws Exception
     */
    public function invokeMethod($method, $params = []) {
        try {
            if (is_array($method)) {
                $class = is_object($method[0]) ? $method[0] : $this->invokeClass($method[0]);
                $reflect = new ReflectionMethod($class, $method[1]);
            } else {
                $reflect = new ReflectionMethod($method);
            }
            $args = $this->bindParams($reflect, $params);
            return $reflect->invokeArgs(isset($class) ? $class : null, $args);
        } catch (ReflectionException $e) {
            if (is_array($method) && is_object($method[0])) {
                $method[0] = get_class($method[0]);
            }

            throw new Exception('method not exists: ' . (is_array($method) ? $method[0] . '::' . $method[1] : $method) . '()');
        }
    }

    /**
     * 调用反射执行类的实例化 支持依赖注入
     * @param $class
     * @param array $params
     * @return object
     * @throws Exception
     */
    public function invokeClass($class, $params = []) {
        try {
            $reflect = new ReflectionClass($class);
            $constructor = $reflect->getConstructor();
            $args = $constructor ? $this->bindParams($constructor, $params) : [];
            return $reflect->newInstanceArgs($args);
        } catch (ReflectionException $e) {
            throw new Exception('class not exists: ' . $class);
        }
    }

    /**
     * 绑定参数  要求传入参数数组键值与定义的键值完全一致
     * @param Reflector $reflect
     * @param array $vars
     * @return array
     * @throws Exception
     */
    protected function bindParams(Reflector $reflect, $vars = []) {
        if ($reflect->getNumberOfParameters() == 0) {
            return [];
        }

        $params = $reflect->getParameters();
        $args = [];
        foreach ($params as $param) {
            //参数名全部转入小写进行匹配
            $paramName = $param->getName();
            $paramClass = $param->getClass();
            if ($paramClass) {
                $objectParamName = $paramClass->getName();
                if (isset($vars[$paramName]) && $vars[$paramName] instanceof $objectParamName) {
                    $args[] = $vars[$paramName];
                } else {
                    //外部未传入参数，而此参数指定了类对象限制，则通过反射自动实例化这个类实例
                    $args[] = $this->make($objectParamName);
                }
            } elseif (isset($vars[$paramName])) {
                $args[] = $vars[$paramName];
            } elseif ($param->isDefaultValueAvailable()) {
                $args[] = $param->getDefaultValue();
            } else {
                throw new InvalidArgumentException('method param miss:' . $paramName);
            }
        }
        return $args;
    }

    /**
     * 删除已经实例化的绑定标识对应的类实例及作为其他绑定标识的别名
     * Drop all of the stale instances and aliases.
     *
     * @param string $abstract
     * @return void
     */
    protected function dropStaleInstances($abstract) {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

    /**
     * 构建一个闭包类
     * @param $abstract
     * @param $concrete
     * @return Closure
     */
    protected function getClosure($abstract, $concrete) {
        return function ($container, $parameters = []) use ($abstract, $concrete) {
            if ($abstract == $concrete) {
                return $container->build($concrete);
            }

            return $container->make(
                $concrete, $parameters
            );
        };
    }

    /**
     * 判断判定的类是否可以直接实例化
     *
     * @param mixed $concrete
     * @param string $abstract
     * @return bool
     */
    protected function isBuildable($concrete, $abstract) {
        $abstract = $this->getAlias($abstract);
        return $concrete === $abstract || $concrete instanceof Closure;
    }

    ////////////////////////////////////////////////////////////
    // 容器对象实例相关操作
    ////////////////////////////////////////////////////////////
    public function offsetGet($abstract) {
        return $this->make($abstract);
    }

    public function offsetSet($abstract, $concrete) {
        $this->bind($abstract, $concrete);
    }

    public function offsetUnset($abstract) {
        $this->delete($abstract);
    }

    public function offsetExists($abstract) {
        return $this->exists($abstract);
    }

    /**
     * 容器中对象实例的数组迭代器
     * @return ArrayIterator|\Traversable
     */
    public function getIterator() {
        return new ArrayIterator($this->instances);
    }

    /**
     * 容器中对象实例的统计
     * @return int
     */
    public function count() {
        return count($this->instances);
    }
}